今天為各位介紹的主題是: "Model-View-Controller" MVC architecture pattern,以及它在 Android 平台上的實作及應用。
MVC 是一個很有趣的 pattern,有趣的點在於每個人對他的理解都有點類似,但卻又有點不同。所以這次特別找尋了許多資料,想從 MVC pattern 的源頭以及發展來幫助說明為何會有這樣的情況,嘗試歸納其各種流派的共同與不同之處,並提及在 Android 中較流行的實作方式。
從 Wikipedia 上我們可以查到,MVC 最初是由 Trygve Reenskaug 在 1979 年歸納出的 pattern [1,2],當時正值個人電腦發展的早期,圖形介面、鍵盤、滑鼠作為鐵三角 (?) 的使用情境逐漸成型。先讓我們來看看此時 Trygve 對於 MVC 的解釋 (對原文做了部分的化簡及個人解讀,建議閱讀原文):
Model: Model 代表了知識,它可以是單一物件或物件的集合。Model 中的節點應表示相同的問題級別,不好的設計會將其與實作細節混在一起。 在 [2] 當中也提及:在一個應用當中的 model 代表了其特殊領域中以軟體模擬或實作的應用核心架構。例如一個加法器的 model 可能為一個單純的 Integer、或是文字編輯的 model 為一個 String。
View: View 是一種對於 model 的 (視覺化) 呈現,它在呈現時會強調 model 中特定的屬性並忽視其餘的內容,因此它可扮演在呈現上所用的過濾器。View 會被附加至 model 上,從 model 取得呈現上必要的資訊,或要求 model 更新。而 view 所有的需求都必須反映到 model 中所定義的語法,所以 view 需要認識 model 的實體。
Controller: Controller 是使用者與系統之間的橋樑。它透過安排 views 的排列以及呈現對使用者進行輸出。它也能夠透過呈現各式選單或有意義的命令或資訊,來接收使用者的輸入。Controller 取得使用者輸入,將其轉譯為適合的資訊並傳遞下去。
Controller 永遠不應該附加/依附於 (supplement to) view,例如它不會知道如何透過畫出一個箭頭來連結兩個 view。
View 永遠不應該知道使用者的輸入,例如滑鼠或按鍵點擊。從 controller 撰寫方法發送訊息給 view 去重現任何順序的使用者指令是可能的。
嗯...40 年前的文章還真是難以理解,還是用 wiki 上的圖片來說明吧:
圖中顯示了 MVC 三個元件與使用者的關係。Controller 作為取得使用者輸入的介面,將訊號輸入至 Model,而在 Model 中的商業邏輯更新其狀態後,Model 主動通知 View 更新,使用者便透過 View 觀察到更新過後的結果。
所以 Controller 和 View 之間完全不認識彼此,且兩者的實作都依賴於平台。僅有 Model 能夠純粹的獨立於平台,單純的處理商業邏輯並保存資料。
MVC 在當時提供了一種輕便簡潔的方式帶來關注分離的效果,使得逐漸複雜的圖形化使用者介面處理可以和資料流分離,並且提供核心的商業邏輯能夠抽離於平台進行獨立測試的可能性,且在資訊流上能夠從使用者輸入一直到畫面輸出始終保持單向性。
它帶來的總總好處,也幫助開發者能夠方便的協作,只要團隊成員間對於 MVC 的理解一致,便能夠快速的達成分工和整合。
MVC 的概念在日後被大量地被移植到不同的使用情境底下,幫助各語言、平台開發者對架構進行分層;而在此時,MVC 一詞所代表的意義也開始有了不同的解讀。
各種語言/平台/架構使用了部分最初 MVC 的概念,它們對於不同的呈現及控制需求,(可能) 重新定義了 MVC 當中個 component 在不同情境下的職責,最終造成了今日每個人對 MVC 的認知都有些相似、又可能有些不同的狀況。
可以看到在這種變形底下,Controller 開始出現在 View 以及 Model 的中間,"取得使用者在系統層的原始輸入" 這項責任被轉移到 View;而 Controller 進一步的將原始輸入轉為商業邏輯中定義的合理輸入,用以更新 Model。
可以看到在三個變形的範例中,處理使用情境流程的前兩步驟是相同的,而在 "如何讓 View 取得更新過後的 Model 用以更新畫面" 的方式則大有不同。在第一個變形中,View 透過觀察 Model 來得知更新,而後續兩個變形中,則是由 Controller 通知 View 此一事件。
這種變形方式稱為 Model2,在 1998 年被提出應用於網路中的 server-client 架構。其中 View 為用戶端 (client) 的網頁呈現,而 Controller 則為伺服器端 (server) 的程序。在 server-client 架構之下,View 難以直接取得 Model,所以出現了此種凡事透過 Controller 的變形。也就是除了來自 View 的使用者輸入外,所有事情都由 Controller 主動完成。
各種變形不一定能夠滿足最初 MVC 的所有設定,但它們在不同語言、平台以及系統架構...等等的環境限制下,發展出了自己對 MVC 的解讀以及應用。
這也是為何我們時常聽到人們對於 MVC 有不同解讀的原因。
程式範例請參考我的 Github repository。
當中的 MVC 有著三種實作,以下分別介紹。
實作範例1:
在 Android 以 open source 之姿問世後,開發者們也開始將自己對於 MVC 的理解整合到 Android 上,但 Android 平台的大鍋炒特性 (程式執行起始點以及 View group 都綁在 Activity 元件上,而且使用者輸入是透過 View 取得),讓開發者們紛紛覺得 "Android 的 Activity 同時實作了 MVC 中的 View 以及 Controller",導致早年的 Android 開發,幾乎只有分成 Activity 以及 Model 兩層。 (Code 很冗長就不貼了)
實作範例2:
這個實作實現了 Model2 的概念,可以看到在創造文章的情境中,Controller 主動更新 Model,並取得更新結果用以通知 View。
class MVCController(private val repository: RunTimeRepository,
private val view: MVCActivity) {
init {
if (repository.getArticles().size == 0) {
repository.mergeDefaultArticles()
}
view.onUpdate(repository.getArticles())
}
fun createNewArticle() {
// 情境2: 創造一篇隨機內容的文章並加入文章列表
val article = ArticleGenerator.randomArticle()
repository.addArticle(article)
view.onUpdate(repository.getArticles())
}
fun selectArticle(article: Article) {
view.showArticleContent(article)
}
fun backFromArticle() {
view.hideArticleContent()
}
}
class MVCActiveRepositoryActivity : AppCompatActivity(), ArticleAdapter.ItemClickListener {
// ... 省略部分實作細節
override fun onCreate(savedInstanceState: Bundle?) {
// View 訂閱 Model 更新的通知
repository.addArticleListChangedSubscriber(onArticleListChanged)
repository.addArticleChangedSubscriber(onSelectedArticleChanged)
subscribeDataChanged()
// ...
}
private fun subscribeDataChanged() {
// 情境2 當中 View 更新的方式:View 得知 model 更新後,主動取得資料
onArticleListChanged
.subscribe {
onUpdate(repository.getArticles())
}.addTo(disposableBag)
}
}
優勢
缺陷
MVC 於 40 年前橫空出世,以一己之身開創了大 pattern 時代,啟發了許多開發者,並在各種環境中孕育了無數後進 (?)。
時至今日,雖然我們已經很難對 MVC 進行嚴謹的定義,且在不同環境下討論 MVC 經常帶來各式各樣的爭論以及誤解。
但 MVC 仍舊是一種流行的 pattern,活躍於各個語言及框架中。
相信開發者們在知道 MVC 的歷史後,都能夠掌握其核心精神,並且以開放的心胸認識不同應用情境下的 MVC 實作吧。
[1] Trygve Reenskaug, Notes and Historical documents, http://heim.ifi.uio.no/~trygver/1979/mvc-2/1979-12-MVC.pdf, 1977
[2] Krasner, Glenn E.; Pope, Stephen T. (Aug–Sep 1988). "A cookbook for using the model–view controller user interface paradigm in Smalltalk-80", https://www.lri.fr/~mbl/ENS/FONDIHM/2013/papers/Krasner-JOOP88.pdf, 1988
[3] http://heim.ifi.uio.no/~trygver/themes/mvc/mvc-index.html
作者:Yolung